// The NSpring Framework for .NET
// Copyright (c) 2003, Jeffrey Varszegi
// All rights reserved.
// 
// Redistribution and use in source and binary forms, with or without modification, 
// are permitted provided that the following conditions are met:
// 
// - Redistributions of source code must retain the above copyright notice, this 
// list of conditions and the following disclaimer.
// 
// - Redistributions in binary form must reproduce the above copyright notice, this 
// list of conditions and the following disclaimer in the documentation and/or other 
// materials provided with the distribution.
// 
// - Neither the name of the NSpring project nor the names of its contributors may 
// be used to endorse or promote products derived from this software without 
// specific prior written permission from the copyright owner.
// 
// - No product derived from this software may be called "NSpring", nor may 
// "NSpring" appear in the name of such a product, without specific prior written 
// permission from the copyright owner.
// 
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
// IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 
// OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 
// OF THE POSSIBILITY OF SUCH DAMAGE. 

namespace NSpring.Logging.DataFormatters {

using System;
using System.Collections;
using System.IO;
using System.Text;
using System.Xml;

using NSpring.Logging.Common;
using NSpring.Logging.XML;

/// <summary>
/// A DataFormatter implementation that formats data objects as XML.  This is the
/// data formatter type used by the XMLEventFormatter class.
/// </summary>
public sealed class XMLDataFormatter : DataFormatter {
    private static string[] millisecondStrings          = Constants.MillisecondStrings;

    private const string AMPERSAND                      = "&";
    private const string APOSTROPHE                     = "'";
    private const string BLANK_STRING                   = "";
    private const string COLON                          = ":";
    private const string GREATER_THAN                   = ">";
    private const string HYPHEN                         = "-";
    private const string LESS_THAN                      = "<";
    private const string PERIOD                         = ".";
    private const string QUOTE                          = "\"";
    private const string SPACE                          = " ";
    private const string UNDERSCORE                     = "_";

    private const char AMPERSAND_CHAR                   = '&';
    private const char APOSTROPHE_CHAR                  = '\'';
    private const char COLON_CHAR                       = ':';
    private const char GREATER_THAN_CHAR                = '>';
    private const char HYPHEN_CHAR                      = '-';
    private const char LESS_THAN_CHAR                   = '<';
    private const char PERIOD_CHAR                      = '.';
    private const char QUOTE_CHAR                       = '"';
    private const char SPACE_CHAR                       = ' ';
    private const char UNDERSCORE_CHAR                  = '_';
    
    private static string NEW_LINE                      = new string(new char[] {((char)13), ((char)10)});
    
    private const string VALID_XML_NAME_CHARACTERS      = " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    private const string TIMESTAMP_FORMAT               = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffzzz";

    private const string START_TAG_END                  = "\">";

    private const string SINGLE_TAG_END                 = "\"/>";
    private const char T_CHAR                           = 'T';
    private const char P_CHAR                           = 'P';
    private const char H_CHAR                           = 'H';
    private const char M_CHAR                           = 'M';
    private const char S_CHAR                           = 'S';
    private const string DT                             = "DT";
    
    private const string AMPERSAND_ESCAPE               = "&amp;";
    private const string APOSTROPHE_ESCAPE              = "&apos;";
    private const string LESS_THAN_ESCAPE               = "&lt;";
    private const string GREATER_THAN_ESCAPE            = "&gt;";
    private const string QUOTE_ESCAPE                   = "&quot;";
    
    private const string nullTypeDescriptor             = "null";
    private const string nullValue                      = "null";
    private const string binaryTypeDescriptor           = "binary";
    private const string booleanTypeDescriptor          = "boolean";
    private const string dateTimeTypeDescriptor         = "dateTime";
    private const string decimalTypeDescriptor          = "decimal";
    private const string doubleTypeDescriptor           = "double";
    private const string timespanTypeDescriptor         = "duration";
    private const string floatTypeDescriptor            = "float";
    private const string integerTypeDescriptor          = "integer";
    private const string stringTypeDescriptor           = "string";
    private const string listTypeDescriptor             = "list";
    private const string dictionaryTypeDescriptor       = "map";
    private const string xmlTypeDescriptor              = "xml";
    
    private string dataElementName                      = "data";
    private string typeAttributeName                    = "type";
    private string entryElementName                     = "entry";
    private string keyElementName                       = "key";
    private string valueElementName                     = "value";

    private string dataStartTag;
    private string dataEndTag;
    private string valueStartTag;
    private string valueEndTag;
    private string entryStartTag;
    private string entryEndTag;
    private string keyStartTag;
    private string keyEndTag;
    //
    private string typeAttribute;

    private string indentation                          = "   ";
    private string newLineIndentation                   = NEW_LINE + "   ";
    
    private XMLFormatOptions xmlFormatOptions;
    
    /// <summary>
    /// Constructs a new instance, which will format XML using the specified
    /// options
    /// </summary>
    /// <param name="options">The formatting options to use when constructing XML</param>
    public XMLDataFormatter(XMLFormatOptions options) {
        if (options == null) {
            throw new ArgumentNullException();
        }
        xmlFormatOptions = options.Copy();
        dateTimeFormat = TIMESTAMP_FORMAT;
        Initialize();
    }
    
    /// <summary>
    /// Constructs a new instance, which will format XML using default formatting options
    /// </summary>
    public XMLDataFormatter() {
        xmlFormatOptions = new XMLFormatOptions();
        dateTimeFormat = TIMESTAMP_FORMAT;
        Initialize();
    }
    
    private void Initialize() {
        lock (monitor) {
            XMLFormatOptions xfo      = xmlFormatOptions;
            string name;

            indentation               = BLANK_STRING.PadLeft(xfo.IndentationWidth);
            newLineIndentation        = NEW_LINE + indentation;

            name                      = xfo.GetFormattedElementName(dataElementName);
            dataStartTag              = "<" + name + " ";
            dataEndTag                = "</" + name + ">";

            name                      = xfo.GetFormattedElementName(entryElementName);
            entryStartTag             = "<" + name + ">";
            entryEndTag               = "</" + name + ">";
            
            name                      = xfo.GetFormattedElementName(keyElementName);
            keyStartTag               = "<" + name + " ";
            keyEndTag                 = "</" + name + ">";
            
            name                      = xfo.GetFormattedElementName(valueElementName);
            valueStartTag             = "<" + name + " ";
            valueEndTag               = "</" + name + ">";
            
            typeAttribute             = xfo.GetFormattedAttributeName(typeAttributeName)            + "=\"";
            
        }
    }
    
    /// <summary>
    /// The XML formatting options used by this instance.  The passed-in object
    /// is copied and stored in this instance; additional modifications of the passed-in
    /// XMLFormatOptions will have no effect on this instance's output until 
    /// this property is set again
    /// </summary>
    public XMLFormatOptions XMLFormatOptions {
        set {
            if (value == null) {
                throw new ArgumentNullException();
            }
            lock (monitor) {
                xmlFormatOptions = value.Copy();
                Initialize();
            }
        }
    }
    
    /// <summary>
    /// Formats the specified data object as XML
    /// </summary>
    /// <param name="o">The data object to format</param>
    /// <returns>An XML representation of the data object</returns>
    public override string Format(object o) {
        StringBuilder sb = new StringBuilder(128);
        Format(o, sb);
        return sb.ToString();
    }
    
    /// <summary>
    /// Formats the specified data object as XML
    /// </summary>
    /// <param name="o">The data object to format</param>
    /// <param name="sb">The string buffer to which to append the XML</param>
    public override void Format(object o, StringBuilder sb) {
        FormatData(o, sb, 1, dataStartTag, dataEndTag);
    }
    
    private void FormatData(object data, StringBuilder sb, int depth, string startTag, string endTag) {
        if (xmlFormatOptions.IsIndentationEnabled) {
            sb.Append(newLineIndentation);
            for(int x = 1; x < depth; x++) {
                sb.Append(indentation);
            }
        }
        if (data == null) {
            sb.Append(startTag).Append(typeAttribute).Append(nullTypeDescriptor).Append(START_TAG_END);
            sb.Append(nullValue);
            sb.Append(endTag);
            return;
        }
        Type dataType = data.GetType();
        
        if (dataType == STRING) {
            sb.Append(startTag).Append(typeAttribute).Append(stringTypeDescriptor).Append(START_TAG_END);
            sb.Append(Escape((string)data));
            sb.Append(endTag);
        }
        else if (dataType.IsPrimitive) {
            string format;
            if (dataType == BOOLEAN) {
                sb.Append(startTag).Append(typeAttribute).Append(booleanTypeDescriptor).Append(START_TAG_END);
                sb.Append(Escape(((bool)data) ? trueValue : falseValue));
                sb.Append(endTag);
            }
            else if (dataType == INT32) {
                sb.Append(startTag).Append(typeAttribute).Append(integerTypeDescriptor).Append(START_TAG_END);
                format = integerFormat;
                if (format != null) {
                    sb.Append(Escape(((int)data).ToString(format)));
                }
                else {
                    sb.Append(Escape(data.ToString()));
                }
                sb.Append(endTag);
            }
            else if (dataType == INT64) {
                sb.Append(startTag).Append(typeAttribute).Append(integerTypeDescriptor).Append(START_TAG_END);
                format = longFormat;
                if (format != null) {
                    sb.Append(Escape(((long)data).ToString(format)));
                }
                else {
                    sb.Append(Escape(data.ToString()));
                }
                sb.Append(endTag);
            }
            else if (dataType == SINGLE) {
                sb.Append(startTag).Append(typeAttribute).Append(floatTypeDescriptor).Append(START_TAG_END);
                format = floatFormat;
                if (format != null) {
                    sb.Append(Escape(((float)data).ToString(format)));
                }
                else {
                    sb.Append(Escape(data.ToString()));
                }
                sb.Append(endTag);
            }
            else if (dataType == DOUBLE) {
                sb.Append(startTag).Append(typeAttribute).Append(doubleTypeDescriptor).Append(START_TAG_END);
                format = doubleFormat;
                if (format != null) {
                    sb.Append(Escape(((double)data).ToString(format)));
                }
                else {
                    sb.Append(Escape(data.ToString()));
                }
                sb.Append(endTag);
            }
            else if (dataType == INT16) {
                sb.Append(startTag).Append(typeAttribute).Append(integerTypeDescriptor).Append(START_TAG_END);
                format = integerFormat;
                if (format != null) {
                    sb.Append(Escape(((Int16)data).ToString(format)));
                }
                else {
                    sb.Append(Escape(data.ToString()));
                }
                sb.Append(endTag);
            }
            else if (dataType == UINT16) {
                sb.Append(startTag).Append(typeAttribute).Append(integerTypeDescriptor).Append(START_TAG_END);
                format = integerFormat;
                if (format != null) {
                    sb.Append(Escape(((UInt16)data).ToString(format)));
                }
                else {
                    sb.Append(Escape(data.ToString()));
                }
                sb.Append(endTag);
            }
            else if (dataType == UINT32) {
                sb.Append(startTag).Append(typeAttribute).Append(integerTypeDescriptor).Append(START_TAG_END);
                format = integerFormat;
                if (format != null) {
                    sb.Append(Escape(((uint)data).ToString(format)));
                }
                else {
                    sb.Append(Escape(data.ToString()));
                }
                sb.Append(endTag);
            }
            else if (dataType == UINT64) {
                sb.Append(startTag).Append(typeAttribute).Append(integerTypeDescriptor).Append(START_TAG_END);
                format = longFormat;
                if (format != null) {
                    sb.Append(Escape(((ulong)data).ToString(format)));
                }
                else {
                    sb.Append(Escape(data.ToString()));
                }
                sb.Append(endTag);
            }
            else if (dataType == BYTE) {
                sb.Append(startTag).Append(typeAttribute).Append(integerTypeDescriptor).Append(START_TAG_END);
                format = integerFormat;
                if (format != null) {
                    sb.Append(Escape(((byte)data).ToString(format)));
                }
                else {
                    sb.Append(Escape(data.ToString()));
                }
                sb.Append(endTag);
            }
            else if (dataType == SBYTE) {
                sb.Append(startTag).Append(typeAttribute).Append(integerTypeDescriptor).Append(START_TAG_END);
                format = integerFormat;
                if (format != null) {
                    sb.Append(Escape(((sbyte)data).ToString(format)));
                }
                else {
                    sb.Append(Escape(data.ToString()));
                }
                sb.Append(endTag);
            }
            else if (dataType == CHAR) {
                sb.Append(startTag).Append(typeAttribute).Append(stringTypeDescriptor).Append(START_TAG_END);
                sb.Append(Escape(data.ToString()));
                sb.Append(endTag);
            }
        }
        else if (dataType == DATE_TIME) {
            sb.Append(startTag).Append(typeAttribute).Append(dateTimeTypeDescriptor).Append(START_TAG_END);
            string format = dateTimeFormat;
            if (format != null) {
                sb.Append(Escape(((DateTime)data).ToString(format)));
            }
            else {
                sb.Append(Escape(data.ToString()));
            }
            sb.Append(endTag);
        }
        else if (dataType == DECIMAL) {
            sb.Append(startTag).Append(typeAttribute).Append(decimalTypeDescriptor).Append(START_TAG_END);
            string format = decimalFormat;
            if (format != null) {
                sb.Append(Escape(((decimal)data).ToString(format)));
            }
            else {
                sb.Append(Escape(data.ToString()));
            }
            sb.Append(endTag);
        }
        else if (data is XmlDocument) {
            sb.Append(startTag).Append(typeAttribute).Append(xmlTypeDescriptor).Append(START_TAG_END);
            XmlDocument document = (XmlDocument)data;
            document.WriteContentTo(new XmlTextWriter(new StringWriter(sb)));
            sb.Append(endTag);
        }
        else if (data is XmlNode) {
            sb.Append(startTag).Append(typeAttribute).Append(xmlTypeDescriptor).Append(START_TAG_END);
            XmlNode document = (XmlNode)data;
            document.WriteTo(new XmlTextWriter(new StringWriter(sb)));
            sb.Append(endTag);
        }
        else if (dataType == TIMESPAN) {
            // The ISO 8601 extended format for specifying timespans (called "durations" there) in XML:
            // PnYnMnDTnHnMnS
            // Example #1: P1Y2M3DT10H30M21S
            // Example #2: P3DT10H30M21.777S
            // The largest block of time in the TimeSpan type is days, and ISO 8601 allows us 
            // to omit unused time portions, so the years are dispensed with here
            TimeSpan ts = (TimeSpan)data;
            sb.Append(startTag).Append(typeAttribute).Append(timespanTypeDescriptor).Append(START_TAG_END);
            sb.Append(P_CHAR);
            sb.Append(ts.Days).Append(DT);
            sb.Append(ts.Hours).Append(H_CHAR);
            sb.Append(ts.Minutes).Append(M_CHAR);
            sb.Append(ts.Seconds);
            if (ts.Milliseconds > 0) {
                sb.Append(PERIOD_CHAR).Append(millisecondStrings[ts.Milliseconds]);
            }
            sb.Append(S_CHAR);
            sb.Append(endTag);
        }
        else if (dataType.IsArray) {
            if (depth < MaximumDepth) {
                sb.Append(startTag).Append(typeAttribute).Append(listTypeDescriptor).Append(START_TAG_END);
                int newDepth = depth + 1;
                Array array = (Array)data;
                for(int x = 0; x < array.Length; x++) {
                    FormatData(array.GetValue(x), sb, newDepth, valueStartTag, valueEndTag);
                }
                if (xmlFormatOptions.IsIndentationEnabled) {
                    sb.Append(newLineIndentation);
                    for(int x = 1; x < depth; x++) {
                        sb.Append(indentation);
                    }
                }
                sb.Append(endTag);
            }
        }
        else if (data is IDictionary) {
            if (depth < MaximumDepth) {
                sb.Append(startTag).Append(typeAttribute).Append(dictionaryTypeDescriptor).Append(START_TAG_END);
                int newDepth = depth + 2;
                IDictionary dictionary = (IDictionary)data;
                IEnumerator keys = dictionary.Keys.GetEnumerator();
                object key;
                while (keys.MoveNext()) {
                    if (xmlFormatOptions.IsIndentationEnabled) {
                        sb.Append(newLineIndentation);
                        for(int x = 0; x < depth; x++) {
                            sb.Append(indentation);
                        }
                    }
                    sb.Append(entryStartTag);
                    key = keys.Current;
                    FormatData(key, sb, newDepth, keyStartTag, keyEndTag);
                    FormatData(dictionary[key], sb, newDepth, valueStartTag, valueEndTag);
                    if (xmlFormatOptions.IsIndentationEnabled) {
                        sb.Append(newLineIndentation);
                        for(int x = 0; x < depth; x++) {
                            sb.Append(indentation);
                        }
                    }
                    sb.Append(entryEndTag);
                }
                if (xmlFormatOptions.IsIndentationEnabled) {
                    sb.Append(newLineIndentation);
                    for(int x = 1; x < depth; x++) {
                        sb.Append(indentation);
                    }
                }
                sb.Append(endTag);
            }
        }
        else if (data is IEnumerable) {
            if (depth < MaximumDepth) {
                sb.Append(startTag).Append(typeAttribute).Append(listTypeDescriptor).Append(START_TAG_END);
                int newDepth = depth + 1;
                IEnumerator e = ((IEnumerable)data).GetEnumerator();
                while (e.MoveNext()) {
                    FormatData(e.Current, sb, newDepth, valueStartTag, valueEndTag);
                }
                if (xmlFormatOptions.IsIndentationEnabled) {
                    sb.Append(newLineIndentation);
                    for(int x = 1; x < depth; x++) {
                        sb.Append(indentation);
                    }
                }
                sb.Append(endTag);
            }
        }
        else {
            TypeFormatter typeFormatter = GetTypeFormatter(dataType);
            if (typeFormatter != null) {
                sb.Append(startTag).Append(typeAttribute).Append(typeFormatter.SupportedTypeDescriptor).Append(START_TAG_END);
                if (typeFormatter.IsPassThroughRequested) { 
                    // This indicates that the data formatter's output shouldn't be escaped
                    sb.Append(typeFormatter.Format(data));
                }
                else {
                    // Since pass-through isn't requested, we should escape the output of the data formatter
                    sb.Append(Escape(typeFormatter.Format(data)));
                }
                sb.Append(endTag);
            }
            else {
                sb.Append(startTag).Append(typeAttribute).Append(stringTypeDescriptor).Append(START_TAG_END);
                sb.Append(Escape(data.ToString()));
                sb.Append(endTag);
            }
        }
    }
    
    private static string Escape(string s) {
        if (s == null) {
            return null;
        }
        if (s.IndexOf(AMPERSAND_CHAR)    >= 0)  {   s = s.Replace(AMPERSAND, AMPERSAND_ESCAPE);         }
        if (s.IndexOf(APOSTROPHE_CHAR)   >= 0)  {   s = s.Replace(APOSTROPHE, APOSTROPHE_ESCAPE);       }
        if (s.IndexOf(LESS_THAN_CHAR)    >= 0)  {   s = s.Replace(LESS_THAN, LESS_THAN_ESCAPE);         }
        if (s.IndexOf(GREATER_THAN_CHAR) >= 0)  {   s = s.Replace(GREATER_THAN, GREATER_THAN_ESCAPE);   }
        if (s.IndexOf(QUOTE_CHAR)        >= 0)  {   s = s.Replace(QUOTE, QUOTE_ESCAPE);                 }
        return s;
    }
    
    private static string ValidateXMLName(string name) {
        if (name == null) {
            throw new ArgumentNullException();
        }
        name = name.Trim();
        if (name.Equals(BLANK_STRING)) {
            throw new ArgumentException("The name must contain characters");
        }
        name = name.Replace(HYPHEN, SPACE);
        name = name.Replace(UNDERSCORE, SPACE);
        for(int x = 0; x < name.Length; x++) {
            if (VALID_XML_NAME_CHARACTERS.IndexOf(name[x]) < 0) {
                throw new ArgumentException("Character " + name[x] + " not allowed");
            }
        }
        return name;
    }
    
    /// <summary>
    /// Constructs a deep copy of this instance
    /// </summary>
    /// <returns>A deep copy of this instance</returns>
    public override DataFormatter Copy() {
        XMLDataFormatter copy = new XMLDataFormatter();
        copy.XMLFormatOptions = xmlFormatOptions.Copy();
        TypeFormatter[] tfa = TypeFormatters;
        for(int x = 0; x < tfa.Length; x++) {
            copy.AddTypeFormatter(tfa[x].Copy());
        }
        copy.dateTimeFormat = this.dateTimeFormat;
        copy.decimalFormat = this.decimalFormat;
        copy.doubleFormat = this.doubleFormat;
        copy.falseValue = this.falseValue;
        copy.floatFormat = this.floatFormat;
        copy.integerFormat = this.integerFormat;
        copy.isKeySortingEnabled = this.isKeySortingEnabled;
        copy.isListSortingEnabled = this.isListSortingEnabled;
        copy.longFormat = this.longFormat;
        copy.trueValue = this.trueValue;
        return copy;
    }
    
}

}
